package propertymgmt.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Random;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.Persistence;
import javax.persistence.PersistenceException;
import javax.persistence.TypedQuery;
import javax.swing.JOptionPane;

import propertymgmt.constants.Constants;
import propertymgmt.constants.Constants.PaymentType;
import propertymgmt.entities.Lease;
import propertymgmt.entities.Payment;
import propertymgmt.entities.Property;
import propertymgmt.entities.Tenant;

/**
 * PropertiesManager.java
 * 
 * Singleton convenience utility class to be used to access the Property objects
 * stored in the database. Effort it made to reduce the number of queries
 * to the database.
 * 
 * @author Brian
 *
 */
public final class PropertiesManager {

	private static PropertiesManager instance = null;
	
	private EntityManagerFactory entityManagerFactory = null;
	private EntityManager entityManager = null;
	private List<Property> currentProperties = null;
	
	/**
	 * Private constructor to ensure no instantiation
	 */
	private PropertiesManager(){
		
	}
	
	/**
	 * Singleton getInstance method to return the one and only
	 * instanceof PropertyManager
	 * 
	 * @return PropertiesManager instance
	 */
	public static PropertiesManager getInstance(){
		if(instance == null){
			instance = new PropertiesManager();
		}
		
		return instance;
	}
	
	/**
	 * Once no longer needed, this method should be closed to cleanup.
	 * This method disconnects from the database.
	 */
	public static void killInstance(){
		if(instance.entityManager != null){
			instance.entityManager.close();
			instance.entityManager = null;
			instance.entityManagerFactory.close();
			instance.entityManagerFactory = null;
		}
		
		instance = null;
	}
	
	/**
	 * Method to return the list of Property objects
	 * @return List<Property>
	 */
	public List<Property> getAllProperties(){
		return currentProperties;
	}
	
	/**
	 * Adds a new Property
	 * 
	 * @param property
	 */
	public void newProperty(Property property){
		if(currentProperties == null){
			currentProperties = new ArrayList<Property>();
		}
		
		if(!currentProperties.contains(property)){
			currentProperties.add(property);
		}else {
			System.err.println("Already have property, not adding again");
		}
	}
	
	/**
	 * Method to save the current database state
	 */
	public void saveDbState(){
		if(entityManager != null){
			if(currentProperties != null){
				for(Property property : currentProperties){
					TypedQuery<Property> query = entityManager.createNamedQuery("Property.getPropertyById", Property.class);
					query.setParameter("propertyId", property.getId());
					
					try{
						Property dbProperty = query.getSingleResult();
						
						if(dbProperty != null){
							entityManager.getTransaction().begin();
							updateDbProperty(dbProperty, property);
							entityManager.getTransaction().commit();
						}
					}catch (NoResultException nre){
						// property doesn't exist in DB, need to create it
						createNewPropertyInDatabase(property);
					}
				}
			}
		}
	}
	
	/**
	 * Method to set the database file this class will work with
	 * @param currentDbFile
	 */
	public void setCurrentDbFile(File currentDbFile){
		try{
			entityManagerFactory = Persistence.createEntityManagerFactory(currentDbFile.getAbsolutePath());
			entityManager = entityManagerFactory.createEntityManager();
			
//			loadFakeProperties();
//			loadAuPropertyData();

			if(entityManager == null){
				System.err.println("Error creating EntityManager for db file: " + currentDbFile.getAbsolutePath());
			}else {
				loadProperties();
			}
		}catch (PersistenceException e){
			JOptionPane.showMessageDialog(null, "Only one instanceof of the Property Management System may access the database " +
					"at a time", "Database Error", JOptionPane.ERROR_MESSAGE);
			System.exit(1);
		}
	}
	
	private void loadFakeProperties(){
		File file = new File("./sampleData/randomAddresses.txt");
		String str = null;
		BufferedReader buffReader = null;
		
		try {
			buffReader = new BufferedReader(new FileReader(file));
			
			Property property = null;
			Random r = new Random();
			while((str = buffReader.readLine()) != null){
				String parts[] = str.split(",");
				String addr = parts[0].trim();
				String county = parts[1].trim();
				String state = parts[2].trim();
				String zip = parts[3].split("-")[0].trim();
				
				String address = addr + ", " + county + ", " + state + ", " + zip;
				
				double sqFt = r.nextInt(5000) + 1200.;
				int numRooms = r.nextInt(5) + 1;
				int numBathrooms = r.nextInt(4) + 1;
				int numHalfBaths = r.nextInt(3);
				
				String addrParts[] = addr.split(" ");
				String desc = "";
				for(int i = 1; i < addrParts.length; i++){
					desc += addrParts[i] + " ";
				}
				
				String extraInfo = "Rental prop in " + county + ", " + state;
				
				property = new Property(desc, address, sqFt, numRooms, numBathrooms, numHalfBaths, extraInfo);
				
				int numLeases = r.nextInt(4) + 1;
				
				createRandomLeases(property, numLeases);
				
				entityManager.getTransaction().begin();
				entityManager.persist(property);
				entityManager.getTransaction().commit();
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if(buffReader != null){
				try {
					buffReader.close();
				} catch (IOException e) {}
			}
		}
	}
	
	private void createRandomPayments(Lease lease){
		Random r = new Random();

		Calendar leaseStartCal = Calendar.getInstance();
		leaseStartCal.setTime(lease.getLeaseStartDate());
		
		Calendar leaseEndCal = Calendar.getInstance();
		leaseEndCal.setTime(lease.getLeaseEndDate());
		
		int leaseStartYear = leaseStartCal.get(Calendar.YEAR);
		int leaseEndYear = leaseEndCal.get(Calendar.YEAR);
		
		ArrayList<Payment> payments = new ArrayList<Payment>();
		
		for(Tenant tenant : lease.getTenants()){
			String paymentName = tenant.getLastName() + " Deposit";
			Calendar cal = Calendar.getInstance();
			cal.setTime(lease.getLeaseStartDate());
			Date dateReceived = cal.getTime();
			double amount = lease.getDepositAmount();
			Integer rentPaymentMonth = null;
			Integer rentPaymentYear = null;
			
			ArrayList<Tenant> associatedTenants = new ArrayList<Tenant>();
			associatedTenants.add(tenant);
			
			PaymentType type = PaymentType.DEPOSIT;
			payments.add(new Payment(type, paymentName, dateReceived, amount,
					associatedTenants, rentPaymentMonth, rentPaymentYear));
			
			int numMonthsInLease = (leaseEndYear - leaseStartYear) * 12;
			
			type = PaymentType.RENT;
			for(int i = 0; i <= numMonthsInLease; i++){
				Calendar rentPaymentCal = (Calendar) cal.clone();
				rentPaymentCal.add(Calendar.MONTH, i);
				paymentName = tenant.getLastName() + " " + Constants.months[rentPaymentCal.get(Calendar.MONTH)] +
						" " + rentPaymentCal.get(Calendar.YEAR) + " Rent";
				
				boolean onTime = r.nextDouble() > .4;
				
				if(onTime){
					rentPaymentCal.set(Calendar.DAY_OF_MONTH, lease.getRentDueDate());
				}else {
					int maxDays = rentPaymentCal.getActualMaximum(Calendar.DAY_OF_MONTH);
					int datePaid = lease.getRentLateDate() + r.nextInt(maxDays - lease.getRentDueDate());
					rentPaymentCal.set(Calendar.DAY_OF_MONTH, datePaid);
				}
				
				dateReceived = rentPaymentCal.getTime();

				boolean fullPayment = r.nextDouble() > .1;
				
				if(fullPayment){
					amount = lease.getRentAmount();
				}else {
					amount = (Math.random() * ((lease.getRentAmount()-1.) - 1.)) + 1.;
					amount = r.nextInt((int) lease.getRentAmount()) + 1;
				}
				
				payments.add(new Payment(type, paymentName, dateReceived, amount,
						associatedTenants, rentPaymentCal.get(Calendar.MONTH), rentPaymentCal.get(Calendar.YEAR)));
			}
			
			type = PaymentType.FINE;
			int randNumFines = r.nextInt(5) + 1;
			for(int i = 0; i < randNumFines; i++){
				paymentName = "Fine";
				Calendar fineCal = (Calendar) cal.clone();
				int randNumDays = r.nextInt(100) + 20;
				fineCal.add(Calendar.DAY_OF_MONTH, randNumDays);
				dateReceived = fineCal.getTime();
				amount = r.nextInt(500) + 350;
				
				payments.add(new Payment(type, paymentName, dateReceived, amount,
						associatedTenants, rentPaymentMonth, rentPaymentYear));
			}
		}
		
		Collections.sort(payments);
		
		for(Payment payment : payments){
			lease.addPayment(payment);
		}
	}
	
	private void createRandomLeases(Property property, int numLeases){
		Lease lease = null;
		
		ArrayList<String> randomNames = getRandomNames();
		
		SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy");
		Random r = new Random();
		
		for(int i = 0; i < numLeases; i++){
			int randStartMonth = r.nextInt(12);
			Calendar startCal = Calendar.getInstance();
			startCal.set(Calendar.MONTH, randStartMonth);
			int maxStartDays = startCal.getActualMaximum(Calendar.DAY_OF_MONTH);
			int randStartDayInMonth = r.nextInt(maxStartDays) + 1;
			int maxYear = 2012;
			int minYear = 1990;
			int randStartYear = (int) ((Math.random() * (maxYear - minYear)) + minYear);
			String randLeaseStartStr = randStartMonth + "/" + randStartDayInMonth + "/" + randStartYear;
			Date leaseStartDate = null;
			try {
				leaseStartDate = sdf.parse(randLeaseStartStr);
			} catch (ParseException e) {
				e.printStackTrace();
			}
			
			Calendar endCal = Calendar.getInstance();
			endCal.setTime(leaseStartDate);
			int leaseYears = r.nextInt(3) + 1;
			endCal.roll(Calendar.YEAR, leaseYears);
			
			Date leaseEndDate = endCal.getTime();
			
			while(property.alreadyHasLeaseDuringDates(leaseStartDate, leaseEndDate)){
				randStartMonth = r.nextInt(12);
				startCal = Calendar.getInstance();
				startCal.set(Calendar.MONTH, randStartMonth);
				maxStartDays = startCal.getActualMaximum(Calendar.DAY_OF_MONTH);
				randStartDayInMonth = r.nextInt(maxStartDays) + 1;
				maxYear = 2012;
				minYear = 1990;
				randStartYear = (int) ((Math.random() * (maxYear - minYear)) + minYear);
				randLeaseStartStr = randStartMonth + "/" + randStartDayInMonth + "/" + randStartYear;
				try {
					leaseStartDate = sdf.parse(randLeaseStartStr);
				} catch (ParseException e) {
					e.printStackTrace();
				}
				
				endCal = Calendar.getInstance();
				endCal.setTime(leaseStartDate);
				leaseYears = r.nextInt(3) + 1;
				endCal.roll(Calendar.YEAR, leaseYears);
				
				leaseEndDate = endCal.getTime();
			}

			double depositAmount = r.nextInt(750) + 500;
			double rentAmount = r.nextInt(750) + 500;
			int rentDueDate = r.nextInt(10) + 1;
			int rentLateDate = rentDueDate + r.nextInt(10) + 1;
			double lateRentFee = r.nextInt(50) + 20;
			double lateRentFeePerDay = r.nextInt(10) + 3;
			
			int numTenants = r.nextInt(property.getNumRooms()) + 1;
			ArrayList<Tenant> tenants = createRandomTenants(randomNames, numTenants);
			
			if(numTenants > property.getNumRooms()) System.err.println("too many " + numTenants + ", " + property.getNumRooms());
			
			boolean petsAllowed = r.nextDouble() > .5;
			double petDepositFee = 0.0;
			boolean petDepositRefundable = false;
			
			if(petsAllowed){
				petDepositFee = r.nextInt(500) + 100;
				petDepositRefundable = r.nextDouble() > .5;
			}
			
			lease = new Lease(property, leaseStartDate, leaseEndDate, depositAmount, rentAmount, rentDueDate,
					rentLateDate, lateRentFee, lateRentFeePerDay, tenants, petsAllowed, petDepositFee, petDepositRefundable);
			
			for(Tenant tenant : tenants){
				tenant.setLease(lease);
			}
			
			createRandomPayments(lease);
			
			property.addLease(lease);
		}
	}
	
	private ArrayList<Tenant> createRandomTenants(ArrayList<String> randomNames, int numTenants){
		ArrayList<Tenant> tenants = new ArrayList<Tenant>();
		
		ArrayList<Integer> usedRandNamesIndices = new ArrayList<Integer>();
		
		Tenant tenant = null;
		Random r = new Random();
		for(int i = 0; i < numTenants; i++){
			String firstName = null;
			String lastName = null;
			
			while(firstName == null){
				int index = r.nextInt(randomNames.size());
				
				if(!usedRandNamesIndices.contains(index)){
					String randomName = randomNames.get(index);
					String parts[] = randomName.split(" ");
					firstName = parts[0].trim();
					lastName = parts[1].trim();
					usedRandNamesIndices.add(index);
				}
			}
			
			int tenantAge = r.nextInt(40) + 18;
			tenant = new Tenant(firstName, lastName, tenantAge);
			
			tenants.add(tenant);
		}
		
		return tenants;
	}
	
	private ArrayList<String> getRandomNames(){
		File file = new File("./sampleData/randomNames.txt");
		String str = null;
		BufferedReader buffReader = null;
		ArrayList<String> names = new ArrayList<String>();
		
		try {
			buffReader = new BufferedReader(new FileReader(file));
			
			while((str = buffReader.readLine()) != null){
				if(!names.contains(str.trim())){
					names.add(str.trim());
				}
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if(buffReader != null){
				try {
					buffReader.close();
				} catch (IOException e) {}
			}
		}
		
		return names;
	}
	
	/**
	 * Method to remove a Property from the database
	 * @param property
	 */
	public void removePropertyFromDB(Property property){
		if(property == null) return;
		
		Property dbProperty = entityManager.find(Property.class, property.getId());
		
		if(dbProperty != null){
			entityManager.getTransaction().begin();
			entityManager.remove(dbProperty);
			entityManager.getTransaction().commit();
			loadProperties();
		}
	}
	
	/**
	 * Method to remove a Tenant from the database
	 * @param tenant
	 */
	public void removeTenantFromDB(Tenant tenant){
		if(tenant == null) return;
		
		Tenant dbTenant = entityManager.find(Tenant.class, tenant.getId());
		
		if(dbTenant != null){
			Lease dbLease = entityManager.find(Lease.class, dbTenant.getLease().getId());
			if(dbLease != null){
				entityManager.getTransaction().begin();
				entityManager.remove(dbTenant);
				dbLease.removeTenant(dbTenant);
				entityManager.getTransaction().commit();
				loadProperties();
			}
		}
	}
	
	/**
	 * Method to remove a Lease from the database
	 * @param lease
	 */
	public void removeLeaseFromDB(Lease lease){
		if(lease == null) return;
		
		Lease dbLease = entityManager.find(Lease.class, lease.getId());
		
		if(dbLease != null){
			Property dbProperty = entityManager.find(Property.class, lease.getProperty().getId());
			if(dbProperty != null){
				entityManager.getTransaction().begin();
				entityManager.remove(dbLease);
				dbProperty.removeLease(dbLease);
				for(Tenant tenant : dbLease.getTenants()){
					Tenant dbTenant = entityManager.find(Tenant.class, tenant.getId());
					if(dbTenant != null){
						entityManager.remove(dbTenant);
					}
				}
				for(Payment payment : dbLease.getPayments()){
					Payment dbPayment = entityManager.find(Payment.class, payment.getId());
					if(dbPayment != null){
						entityManager.remove(dbPayment);
					}
				}
				entityManager.getTransaction().commit();
				loadProperties();
			}
		}
	}
	
	/**
	 * Updates a database Property instance with the data in <code>property</code>
	 * @param dbProperty
	 * @param property
	 */
	private void updateDbProperty(Property dbProperty, Property property){
		if(dbProperty == null || property == null) return;
		
		dbProperty.setPropertyName(property.getPropertyName());
		dbProperty.setPropertyAddress(property.getPropertyAddress());
		dbProperty.setSquareFeet(property.getSquareFeet());
		dbProperty.setNumRooms(property.getNumRooms());
		dbProperty.setNumBaths(property.getNumBaths());
		dbProperty.setNumHalfBaths(property.getNumHalfBaths());
		dbProperty.setExtraInfo(property.getExtraInfo());
		
		for(Lease lease : property.getLeases()){
			Lease dbPropertyLease = dbProperty.getLease(lease.getId());
			
			if(dbPropertyLease == null){
				dbProperty.addLease(lease);
			}else {
				updateDbPropertyLease(dbPropertyLease, lease);
			}
		}
	}
	
	/**
	 * Updats a database Lease instance with the data in <code>lease</code>
	 * @param dbPropertyLease
	 * @param lease
	 */
	private void updateDbPropertyLease(Lease dbPropertyLease, Lease lease){
		dbPropertyLease.setLeaseStartDate(lease.getLeaseStartDate());
		dbPropertyLease.setLeaseEndDate(lease.getLeaseEndDate());
		dbPropertyLease.setDepositAmount(lease.getDepositAmount());
		dbPropertyLease.setRentAmount(lease.getRentAmount());
		dbPropertyLease.setRentDueDate(lease.getRentDueDate());
		dbPropertyLease.setRentLateDate(lease.getRentLateDate());
		dbPropertyLease.setPetsAllowed(lease.isPetsAllowed());
		dbPropertyLease.setPetDepositFee(lease.getPetDepositFee());
		dbPropertyLease.setPetDepositRefundable(lease.isPetDepositRefundable());
		dbPropertyLease.setTenants(lease.getTenants());
	}
	
	/**
	 * Creates a new Property within the database.
	 * @param property
	 */
	private void createNewPropertyInDatabase(Property property){
		if(entityManager != null && property != null){
			entityManager.getTransaction().begin();
			entityManager.persist(property);
			entityManager.getTransaction().commit();
		}
	}
	
	private void loadProperties(){
		if(entityManager != null){
			currentProperties = null;
			TypedQuery<Property> query = entityManager.createNamedQuery("Property.getAllProperties", Property.class);
			
			currentProperties = query.getResultList();
		}
	}
}